package lock

import (
	"container/list"
	"errors"
	"runtime"
	"strconv"
	"strings"
	"sync"
)

/*
[!!!] be carefully when use 'Trace_lock_path = true'
if a lot of goroutine exist, and they are all very short(run in short time), and all use one lock, the memory taken by self.path will be very large,
and unfortunately, we cannot simplely reset path by new map when running, that may be panic,
if we want reset, we should stop all Lock/Unlock action, and use many mutexes for each goroutine, it's too complex
or use one mutex to lock all goroutine with bad performance or slow speed when testing,
so result is NO RESET, it just for debug
*/
var Trace_lock_path = false

type Lock struct {
	mutex          sync.Mutex
	path           map[int32]*list.List
	callstackMutex *sync.Mutex
}

func NewLock() *Lock {
	if Trace_lock_path {
		return &Lock{
			path:           make(map[int32]*list.List),
			callstackMutex: &sync.Mutex{},
		}
	} else {
		return &Lock{}
	}
}

func (self *Lock) callerLocation() string {
	_, file, line, ok := runtime.Caller(2)
	if !ok {
		file = "???"
		line = 1
	} else {
		slash := strings.LastIndex(file, "/")
		if slash >= 0 {
			file = file[slash+1:]
		}
	}
	return file + ":" + strconv.Itoa(line)
}

// get lock call stack list for one goroutine, this list will be only modified and accessed in the same one goroutine, goroutine-safe
func (self *Lock) getLockCallStack() *list.List {
	var goid int32 = 0 //runtime.GoId() // when want to use it, see doc/获得goroutine的id的方法.txt
	lockCallStack, ok := self.path[goid]
	if !ok {
		lockCallStack = func() *list.List {
			self.callstackMutex.Lock()
			defer self.callstackMutex.Unlock()

			lockCallStack, ok := self.path[goid]
			if !ok {
				lockCallStack = list.New()
				self.path[goid] = lockCallStack
			}
			return lockCallStack
		}()
	}

	return lockCallStack
}

func (self *Lock) Lock() {
	if Trace_lock_path {
		lockCallStack := self.getLockCallStack()
		lockCallStack.PushBack(self.callerLocation())
		if lockCallStack.Len() > 1 {
			var callSequenceInfo string
			head := lockCallStack.Front()
			for head != nil {
				callSequenceInfo += "\t" + head.Value.(string) + "\n"
				head = head.Next()
			}
			info := "[lock blocked]:\n" + callSequenceInfo
			panic(errors.New(info))
		}
	}
	self.mutex.Lock()
}

func (self *Lock) Unlock() {
	if Trace_lock_path {
		lockCallStack := self.getLockCallStack()
		lockCallStack.Remove(lockCallStack.Back())
	}
	self.mutex.Unlock()
}
